歷經一連串初階副本任務,我們已經習得不少進階的實戰技巧。接著,將迎來新的挑戰,認識 Page Object Models (以下簡稱 POM)。透過建立專屬的 POM,不僅能讓測試流程更加結構化,還能大幅提升程式的可讀性與維護性,讓我們的測試更加井然有序。
隨著測試案例逐漸增加,往往會出現大量重複的頁面操作與定位邏輯。每當編寫新的測試案例時,似乎都得從頭再來一遍。而一旦頁面有所調整 ── 無論是文字修改,還是樣式變更 ── 都必須在浩瀚的測試碼中逐一修正,既繁瑣耗時又容易出錯。因此,藉由 POM 將頁面操作抽象化,並集中管理元素與行為,我們能讓測試碼更精簡、更清晰,同時具備更高的延展性與維護性。
POM 是一種設計模式,它的概念是將頁面視為物件,在物件裡定義這個頁面的元素定位 (Locator) 與 操作方法 (actions),如此一來,在編寫測試時,就不須再注意頁面細節,只需要呼叫事先定義好的物件方法,專注於測試流程,順利完成測試情境,而元素或操作有異動時,也只需修改物件即可。
實作 POM 的步驟:
只須按照上方步驟,就能一步步建立自己的專屬 POM。
以撰寫 TypeScript 官網的測試為例,先來看看沒有定義 POM 的狀況:
import { test, expect } from '@playwright/test';
test.describe('TS Website', () => {
test.beforeEach(async ({ page }) => {
await page.goto('https://www.typescriptlang.org/');
});
// handbook page 第 1 個測試:前往 handbook 裡的 The Basics
test('handbook page - has title', async ({ page }) => {
// 點擊 handbook tab,並驗證標題
await page.locator('#tab3').click();
await expect(page.getByRole('heading', { name: 'The TypeScript Handbook' })).toBeVisible();
// 點擊 sidebar 裡的 The Basics,並驗證標題
await page.locator('nav[id="sidebar"]').getByRole('link', { name: 'The Basics' }).click();
await expect(page.getByRole('heading', { name: 'The Basics', exact: true })).toBeVisible();
// 接續其他測試...
});
// handbook page 第 2 個測試:前往 handbook 裡的 Narrowing
test('handbook page - via npm', async ({ page }) => {
// 點擊 handbook tab,並驗證標題
await page.locator('#tab3').click();
await expect(page.getByRole('heading', { name: 'The TypeScript Handbook' })).toBeVisible();
// 點擊 sidebar 裡的 Narrowing,並驗證標題
await page.locator('nav[id="sidebar"]').getByRole('link', { name: 'Narrowing' }).click();
await expect(page.getByRole('heading', { name: 'Narrowing', exact: true })).toBeVisible();
// 接續其他測試...
});
// handbook page 第 3 個測試:前往 handbook 裡的 More on Functions
test('handbook page - has header', async ({ page }) => {
// 點擊 handbook tab,並驗證標題
await page.locator('#tab3').click();
await expect(page.getByRole('heading', { name: 'The TypeScript Handbook' })).toBeVisible();
// 點擊 sidebar 裡的 More on Functions,並驗證標題
await page.locator('nav[id="sidebar"]').getByRole('link', { name: 'More on Functions' }).click();
await expect(page.getByRole('heading', { name: 'More on Functions', exact: true })).toBeVisible();
// 接續其他測試...
});
});
可以看到這 3 個案例一直在重複:
如果後續有更多 handbook 頁面的測試,就會一直重複上面相同的步驟,這時,就是建立 POM 的時候了。
Playwright 同時提供 TypeScript 與 JavaScript 的 POM 方法,這裡先以 TypeScript 建立 POM 的方式為主,JavaScript 的步驟相同,唯獨語法稍有不同,如有需要請參考 Playwright 官網 Page object models 的介紹。
建立頁面類別:先建立一個資料夾統一管理 POM,接著在資料夾底下建立一個管理 Handbook 頁面的檔案,在檔案內先針對 TypeScript Handbook 建立一個 Page 物件。
// pages/HandbookPage.ts
import { Page, Locator, expect } from '@playwright/test';
export class HandbookPage {
readonly page: Page;
readonly tabHandbook: Locator;
readonly sidebar: Locator;
constructor(page: Page) {
this.page = page;
}
定義元素定位:在 constructor 內加入元素定位
constructor(page: Page) {
this.page = page;
// 加入元素定位
this.handbook = page.locator('#tab3');
this.sidebar = page.locator('nav[id="sidebar"]');
}
封裝操作方法:在 class 裡將常用操作封裝起來
export class HandbookPage {
readonly page: Page;
readonly handbook: Locator;
readonly sidebar: Locator;
constructor(page: Page) {
this.page = page;
this.handbook = page.locator('#tab3');
this.sidebar = page.locator('nav[id="sidebar"]');
}
// 開啟 handbook 入口並同時驗證頁面已開啟
async openHandbook(): Promise<void> {
await this.handbook.click();
}
// 取得目標章節的連結 locator(方便被外部調用)
getTopicLink(topic: string): Locator {
return this.sidebar.getByRole('link', { name: topic });
}
// 取得標題 locator(讓測試可以做斷言)
getHeadingLocator(topic: string): Locator {
return this.page.getByRole('heading', { name: topic, exact: true });
}
// 斷言:標題可見(封裝常用驗證)
async expectHeadingVisible(topic: string): Promise<void> {
await expect(this.getHeadingLocator(topic)).toBeVisible();
}
// 進入指定章節頁面(包含點擊與等待頁面顯示)
async goToTopic(topic: string): Promise<void> {
await this.handbook.click();
await this.page.waitForLoadState('load');
await this.expectHeadingVisible('The TypeScript Handbook');
const link = this.getTopicLink(topic);
await link.click();
}
}
在測試中使用:引入建立的 POM,宣告Page Object 實例,並且在每個測試前都建立新的 Page Object 實例,再呼叫 goToTopic()
將操作流程替換
import { test } from '@playwright/test';
import { HandbookPage } from './pages/HandbookPages';
test.describe('TS Website', () => {
// 宣告Page Object 實例
let handbookPage: HandbookPage;
test.beforeEach(async ({ page }) => {
await page.goto('https://www.typescriptlang.org/');
// 在每個測試前都建立新的 Page Object 實例
handbookPage = new HandbookPage(page);
});
test('handbook page - has title', async ({ page }) => {
// 前往 handbook page 的 The Basics,並驗證標題
await handbookPage.goToTopic('The Basics');
// 接續其他測試...
});
test('handbook page - via npm', async ({ page }) => {
// 前往 handbook page 的 Narrowing,並驗證標題
await handbookPage.goToTopic('Narrowing');
// 接續其他測試...
});
test('handbook page - has header', async ({ page }) => {
// 前往 handbook page 的 More on Functions,並驗證標題
await handbookPage.goToTopic('More on Functions');
// 接續其他測試...
});
});
這樣一來,就完成了 POM 的建立流程,從頁面類別的設計、元素定位的集中管理,到操作方法的封裝與最後在測試中呼叫使用,整個 POM 的開發與應用流程已經走完一輪。
到這裡,我們學會了如何建立自己的 Page Object Models (POM),在測試案例中成功應用。透過這種設計方式,不僅讓測試程式碼的可讀性與可維護性大幅提升,也為後續的自動化流程打下了穩固基礎。接下來,我們要邁入自動化測試中最關鍵的一步,讓測試能夠在 CI/CD 環境中自動執行。